﻿using System;
using Jsdl.Domain;
using Jsdl.Domain.Input.JsonService;
using Jsdl.Domain.Intermediate;
using Jsdl.Domain.Type;

namespace Jsdl.Generators
{
	public class TypeScriptCodeGenerator : CodeGenerator
	{
		public TypeScriptCodeGenerator()
		{
			GenerateServiceClass = false;
			GenerateBaseUrl = true;
			GenerateInterfaces = true;
			UseKnockoutJs = false;
			LibraryPath = "libs";
		}

		public bool GenerateServiceClass { get; set; }
		public bool GenerateBaseUrl { get; set; }
		public bool GenerateInterfaces { get; set; }
		public bool UseKnockoutJs { get; set; }
		public string LibraryPath { get; set; }

		public override string GenerateJsonServiceCode()
		{
			var header = @"/// <reference path=""[libs]/jquery.d.ts"" />" + "\n";
			if (UseKnockoutJs)
				header += @"/// <reference path=""[libs]/knockout.d.ts"" />" + "\n";

			header +=
@"

	export interface JsdlException {
		code: string;
		message?: string;
		stackTrace?: string;
		data?: any; 
	}

";

			return header.Replace("[libs]", LibraryPath) + "[classes]";
		}

		public override string GenerateJsonServiceClassCode()
		{
			
			if (!GenerateServiceClass)
				return (GenerateBaseUrl ? 
@"export var baseUrl = "".""; 
export var beforeSend: any = undefined;
" : "export var beforeSend: any = undefined;\n[body]"); 

			var clientClassTemplateWithBaseUrl =
@"export class [name]
{
	beforeSend = undefined;
	baseUrl: string;

	constructor(baseUrl?: string)
	{
		this.baseUrl = baseUrl; 
	}

[body]	
}

";
			var clientClassTemplateWithoutBaseUrl =
@"export class [name]
{
	beforeSend = undefined; 
[body]	
}

";

			return (GenerateBaseUrl ? clientClassTemplateWithBaseUrl : clientClassTemplateWithoutBaseUrl)
				.Replace("[name]", ClientClassName);
		}

		public override string GenerateJsonServiceOperationCode(string operationName, JsdlOperation operation)
		{
			operationName = ConvertName(operationName);

			var postVariable = "var data = url; ";
			var parameters = "";
			var parametersCode = "";

			if (operation.Transport == JsdlOperationTransport.post)
				postVariable = UseKnockoutJs ?
					"var data = ko.toJSON(Array.prototype.slice.call(arguments, 0, completed !== undefined ? arguments.length - 1 : arguments.length));" :
					"var data = JSON.stringify(Array.prototype.slice.call(arguments, 0, completed !== undefined ? arguments.length - 1 : arguments.length));";
	
			if (operation.Parameters != null) // POST / GET
			{
				foreach (var param in operation.Parameters)
				{
					var name = ConvertName(param.Name, true);
					parameters += "_" + name + ": " + GenerateType(param) + ", ";

					if (operation.Envelope == JsdlOperationEnvelope.query || operation.Transport == JsdlOperationTransport.get)
						parametersCode += "url += \"" + name + "=\" + encodeURIComponent(_" + name + ".toString()) + \"&\";\n";
					else if (operation.Envelope == JsdlOperationEnvelope.url)
						throw new NotImplementedException("envelope == url not implemented in TS");
				}
			}

			if (operation.Parameter != null) // POST only
			{
				if (operation.Parameters == null || operation.Envelope == JsdlOperationEnvelope.query)
					postVariable = UseKnockoutJs ? "var data = ko.toJSON(_parameter);" : "var data = JSON.stringify(_parameter);";
				parameters += "_parameter: " + GenerateType(operation.Parameter, operationName + "Request") + ", ";
			}

			var returnCode = "data: string, exception: JsdlException, result: JQueryXHR, textStatus: string";
			var returnConvert = "data";
			if (operation.Returns != null)
			{
				var isPrimitiveType = false;
				var type = GenerateType(operation.Returns, operationName + "Response", out isPrimitiveType);

				if (!isPrimitiveType && type != "any" && !type.EndsWith("[]") && !type.StartsWith("Array") && !GenerateInterfaces)
					returnConvert = "<" + type + ">" + type + ".fromJson(data)";
				else
					returnConvert = "<" + type + ">jQuery.parseJSON(data)";

				returnCode = "data: " + type + ", exception: JsdlException, result: JQueryXHR, textStatus: string";
			}

			parametersCode = parametersCode.Trim('\n');

			var dataAssignment = "";
			if (operation.Transport == JsdlOperationTransport.post)
				dataAssignment = "data: data,";

			var signature = "";
			var baseUrl = "this.baseUrl + ";
			var beforeSend = "this.beforeSend";
			if (!GenerateServiceClass)
			{
				signature = "export var [name] = function";
				baseUrl = "baseUrl + ";
				beforeSend = "beforeSend";
			}
			else
				signature = "[name]";

			if (!GenerateBaseUrl)
				baseUrl = "";

			var alertException = "alert(\"Operation '[name]' failed: \\n\\n\" + result.responseText);";
			var template =
@"[signature]([parameters]completed?: ([returnCode]) => void)
{
	var url = [baseUrl]""[target][delim]"";
[parametersBody]
[postVariable]
	$.ajax({
		url: url, 
		[dataAssignment]
		beforeSend: [beforeSend], 
		type: ""[type]"", 
		dataType: ""html""
	}).done(function(data, textStatus, result) { 
		if (completed !== undefined) {
			var d = null; 
			try { 
				d = [returnConvert];
			} catch(e) { 
				[alertException]
			}
			completed(d, null, result, textStatus);
		}
	}).fail(function (result, textStatus) {
		var exception = null; 
		if (result.status >= 400) {
			if (result.responseText != """")
				try { exception = jQuery.parseJSON(result.responseText); } catch (e) { exception = { code: ""error_"" + result.status }; }
			else
				exception = { code: ""error_"" + result.status };
			console.log(""JSDL server exception (operation: [name]): "" + result.responseText);
		}
		else 
			[alertException]
		
		if (completed !== undefined)
			completed(null, exception, result, textStatus);
	});
}

";

			if (!string.IsNullOrEmpty(operation.Description))
				template = "/** " + operation.Description + " */\n" + template;

			return template
				.Replace("[alertException]", alertException)
				.Replace("[signature]", signature)
				.Replace("[baseUrl]", baseUrl)
				.Replace("[beforeSend]", beforeSend)
				.Replace("[type]", operation.Transport.ToString())
				.Replace("[dataAssignment]", dataAssignment)
				.Replace("[delim]", operation.Target.Contains("?") ? "&" : "?")
				.Replace("[returnCode]", returnCode)
				.Replace("[returnConvert]", returnConvert)
				.Replace("[name]", ConvertName(operationName, true))
				.Replace("[target]", GenerateBaseUrl ? "/" + operation.Target : operation.Target)
				.Replace("[postVariable]", postVariable)
				.Replace("[parameters]", parameters)
				.Replace("[parametersBody]", IntendCode(parametersCode));
		}

	    protected override string GenerateType(JsdlType type, string suggestedClassName, out bool isPrimitiveType)
		{
			if (type.Type == "object" && type.Properties == null)
			{
				isPrimitiveType = false;
				return "any";
			}

			var result = base.GenerateType(type, suggestedClassName, out isPrimitiveType);

			if (type.Type.StartsWith("array") || type.Type.StartsWith("dictionary"))
				isPrimitiveType = false;
			return result; 
		}

		public override string GeneratePropertyCode(string propertyName, string targetPropertyType, JsdlProperty property)
		{
			var template =
@"
[comment][propertyName]: [type];
";
			return template
				.Replace("[comment]", !string.IsNullOrEmpty(property.Description) ? "/** " + property.Description + " */\n" : "")
				.Replace("[type]", targetPropertyType)
				.Replace("[propertyName]", ConvertName(propertyName, true));
		}

		public override string GenerateClassCode(string outputTypeName, string outputTypeBody)
		{
			var classTemplate =
@"export class [name]
{[body]
	toJson() 
	{
		return """";
	}

	fromJson(json: string)
	{
		return new [name]();
	}
}
";
			var interfaceTemplate =
@"export interface [name]
{[body]}
";

			return (GenerateInterfaces ? interfaceTemplate : classTemplate)
				.Replace("[name]", outputTypeName)
				.Replace("[body]", outputTypeBody);
		}

		public override string GetTypeName(JsdlType type)
		{
			if (type.Type == "string")
				return "string";

			if (type.Type == "boolean")
				return "boolean";
			if (type.Type == "number")
				return "number";
			if (type.Type == "integer")
				return "number";

			return GetTypeName(type.Type);
		}

		public override string GetArrayTypeName(string listType, string itemType)
		{
			if (listType == "array")
				return itemType + "[]";
			else // dictionary
				return "{ [index: string]: " + itemType + "; }";
		}

		public override string GetTypeName(string className)
		{
			return ConvertName(className);
		}
	}
}
